vueJS3 - How to trigger a function on event from sibling component - vue.js

I want to trigger a function that GETs data from a http-server in a component, as soon as a button in a sibling component was pressed.
SignUpForm.vue has a button that triggers customSubmit()
customSubmit(){
//POST to API
const user = {
method: "POST",
headers: { "Content-Type": "application/json"},
body: JSON.stringify({newUser: this.newUser})
};
fetch("http://localhost:3080/api/user", user)
.then(response => response.json())
.then(data => console.log(data));
this.$emit('refresh', true)
this.clearForm();
}
The parent component looks as follows:
<template>
<div>
<SignUpForm #refresh="triggerRefresh($event)" />
<!-- <Exp /> -->
<Datatable :myRefresh="myRefresh" />
</div>
</template>
<script>
import SignUpForm from "./components/SignUpForm.vue";
import Datatable from "./components/Datatable.vue";
import Exp from "./components/exp copy.vue";
export default {
name: "App",
components: { Datatable, SignUpForm, Exp },
data() {
return {
myRefresh: false,
};
},
methods: {
triggerRefresh(bool) {
this.myRefresh = bool;
console.log(this.myRefresh);
},
},
};
</script>
Now i want the sibling component Datatable.vue
to fetch data from the server as soon, as this.$emit('refresh', true) is fired in SignUpForm.vue
Here's the script from Datatable.vue
export default {
data() {
return {
//Liste aller User
userData: null,
//temporärer User für das Details-Feld
printUser: [{ name: "", email: "", number: "" }],
//Property für den "read-Button"
showDetails: false,
//Property für den "Update-Button"
readOnly: true,
};
},
props: ["myRefresh"],
methods: {
pushFunction() {
fetch("http://localhost:3080/api/users")
.then((res) => res.json())
.then((data) => (this.userData = data));
},
readData(k) {
this.printUser.length = 0;
this.showDetails = true;
this.printUser.push(this.userData[k]);
},
editData(rowUser) {
if (!rowUser.readOnly) {
rowUser.readOnly = true;
const user = {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ userData: this.userData }),
};
fetch("http://localhost:3080/api/users/patch", user)
.then((response) => response.json())
.then((data) => console.log(data));
} else {
rowUser.readOnly = false;
}
},
deleteData(k) {
fetch("http://localhost:3080/api/users/" + k, { method: "DELETE" }).catch(
(err) => console.log(err)
);
this.pushFunction();
},
//blaue Reihen
toggleHighlight(rowUser) {
if (rowUser.readOnly === false) {
return;
}
rowUser.isHighlight = !rowUser.isHighlight;
},
scrollDown() {
window.scrollTo(0, document.body.scrollHeight);
},
},
mounted() {
fetch("http://localhost:3080/api/users")
.then((res) => res.json())
.then((data) => (this.userData = data));
},
};
I really hope somebody can help a newbie out!

Two considerations, is it possible? and is it prudent?
Is it possible?
Yes, it is and you can implement it couple different ways.
Is it prudent?
No.
If you're going down this road, most likely the architecture is ineffective. In an ideal setup, your components should be responsible for managing the view only. That means what the user sees and collecting their input. The business logic should not live in the components. So if you have things like ajax calls and you put them into your component, you've coupled the logic to the view. One possible issue is that if the component is re-added for some reason, any in-progress ajax calls could be disrupted in an unexpected manner. While such scenarios can be handled, the bigger issue IMHO is that that when you are coupling business logic with the view layer you are creating an application that becomes increasingly difficult to reason about; This problem you have with sending event between sibling components is just one example.
Other options
The most common way, though not the only way, of dealing with this is by using a global store via Vuex.
Instead of initializing the Ajax request from your component, you call the Vuex action.
The action would usually set loading state either using single state variable (ie loadState=STATE.STARTED) or using isLoading=true, except instead of assigning the variable, vuex would do it through a mutation, so store.commit('setLoadState', STATE.LOADING). this will update the state in all components that are listening for changes in either the store directly or using a getter. Then the ajax request is made, and when it is done the store is updated again, either with store.commit('setLoadState', STATE.ERROR) or on success, store.commit('setLoadState', STATE.DONE) and store.commit('setUsers', response). Then your components only need to listen for changes, you can display a spinner if $store.loadState == STATE.LOADING
As long as the data for the subsequent call is related to data specific to the component (like specific user ID or name) you can handle the next call from the component. Instead of triggering the second API request from the component by watching for an event from the sibling, you can have the component watch the vuex store or data for a change. Then when $store.loadState becomes STATE.DONE, you can trigger another action for the other API call. I would only do this though if there is any part of the data that is specific to the API call, otherwise if the call comes right after in all circumstances, you might as-well call it as part of the same action

Related

How do I re-render a table component on screen after changing json data in Vue?

I am at a beginner level and learning vue at the moment.
All I am trying to do at the moment is having the table reload(re-render) itself so that the change from db.json file is applied on the table on the screen. I created a modal that adds data (name, email, contacts) to the db.json file once the save button is clicked.
However, the problem is that I have to manually reload the page (by pressing ctrl+R) in order for the changed data to be applied on the table.
Here is the script section of "Modal.vue" file (child component)
<script>
export default {
name: "TeamAddModal",
props: {
visible: Boolean,
variant: String,
},
data() {
return {
openClose: this.visible,
memberName: "",
memberEmail: "",
memberContacts: "",
};
},
methods: {
showModal() {
this.openClose = !this.openClose;
},
handleSave() {
if (this.memberName.length > 0) {
console.log(this.memberName, this.memberEmail, this.memberContacts);
this.openClose = !this.openClose;
let userData = {
name: this.memberName,
email: this.memberEmail,
contacts: this.memberContacts,
};
fetch("http://localhost:3000/teaminfo", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(userData),
})
.then(() => {
this.$router.push("/");
})
.catch((err) => console.log(err));
this.memberName = "";
this.memberEmail = "";
this.memberContacts = "";
}
},
},
watch: {
visible: function (newVal, oldVal) {
this.openClose = newVal;
console.log("new" + newVal + "==" + oldVal);
},
},
};
</script>
I would like the parent component(with the table) to re-render and show the change in json data once the 'save' button is clicked from the modal.
I have tried searching up for the solution in google and youtube, and it seems that :key may do the work, but I'm not sure if that really is the solution. I'll be very grateful for the help.
Once you get success response from the POST method API call on save, There could be a two solutions :
You can emit the userData object from modal component to parent and push this emitted object in the table data array.
On successful save, You can emit a success flag from modal component to parent component and then invoke a GET call to receive real time data from an API itself and bind the whole data into table.
In modal component :
handleSave() {
...
...
let userData = {
name: this.memberName,
email: this.memberEmail,
contacts: this.memberContacts,
};
fetch("http://localhost:3000/teaminfo", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(userData),
})
.then(() => {
this.$emit('save-success'); ✅
this.$router.push("/"); ❌
})
}
In parent component :
<modal-component #save-success="getTableData"></modal-component>
getTableData() {
// Make an API call to get the real time updated data and assign that to the table data items variable.
}

Vuex: getter to dispatch action if no data is in the state

I use vuex for my state as well as fetching data and display it in my application.
But I wonder if I'm doing it right. At the moment I dispatch an fetchDataAsync action from the component mounted hook, and I have an getter to display my data. Below is a code example of how I do it currently.
I wonder if it's necessary. What I really want is a getter, that looks at the state, checks if the data is already there and if the data is not there it is able to dispatch an action to fetch the missing data.
The API of vuex does not allow it so I need to put more logic into my components. E.g. if the data is depended of a prop I need a watcher that looks at the prop and dispatches the fetchDataAsync action.
For me it just feels wrong and I wonder if there is a better way.
let store = new Vuex.Store({
state: {
posts: {}
},
mutations: {
addPost(state, post) {
Vue.set(state.posts, post.id, post);
}
},
actions: {
fetchPostAsync({ commit }, parameter) {
setTimeout(
() =>
commit("addPost", { id: parameter, message: "got loaded asynchronous" }),
1000
);
}
},
getters: {
// is it somehow possible to detect: ob boy, I don't have this id,
// I'd better dispatch an action trying to fetch it...?
getPostById: (state) => (id) => state.posts[id]
}
});
new Vue({
el: "#app",
store,
template : "<div>{{ postToDisplay ? postToDisplay.message : 'loading...' }} </div>",
data() {
return {
parameter: "a"
};
},
computed: {
...Vuex.mapGetters(["getPostById"]),
postToDisplay() {
return this.getPostById(this.parameter);
}
},
methods: {
...Vuex.mapActions(["fetchPostAsync"])
},
mounted() {
this.fetchPostAsync(this.parameter);
}
});
I also created a codepen
Personally I think the solution you suggested (adding a watcher that dispatches fetchPostAsync if the post is not found) is the best one. As another commenter stated, getters should not have side effects.

Vue reactivity issues in component making asynchronous server requests

A small amount of context: I have a Vue view called Articles. When the component is mounted to the DOM, I fetch all posts from the database using the axios library (in conjunction with Laravel controllers and API routes). The articles view contains a data property called active, which points towards the post that is currently selected. Clicking on a different post in the sidebar updates active and subsequently the new post is shown.
Now, every post has many comments, and those comments in turn can be linked to subcomments if you will. However, the mounted lifecycle hook in Articles.vue gets invoked only once and when I try to place the server request in updated(), everything seemingly works but I'd eventually get a 429 status (too many requests). My guess is that for each comment that is retrieved, the code in updated() get's invoked again.
I guess my question is as follows: How can I make Post.vue reactive, since right now the mounted lifecycle hook will be invoked only once even when another post is selected.
Here's the code:
Articles.vue
export default {
name: "Articles",
components: {SidebarLink, PageContent, Sidebar, Post, Searchbar, Spinner},
data() {
return {
posts: [],
active: undefined,
loading: true
}
},
mounted() {
this.fetchPosts();
},
methods: {
async fetchPosts() {
const response = await this.$http.get('/api/posts');
this.posts = response.data;
this.active = this.posts[0];
setTimeout(() => {
this.loading = false;
}, 400);
},
showPost(post) {
this.active = post;
}
}
}
Post.vue
export default {
name: "Post",
components: {Tag, WennekesComment},
props: ['post'],
data() {
return {
expanded: true,
comments: []
}
},
mounted() {
this.fetchComments();
},
methods: {
async fetchComments() {
let response = await this.$http.get('/api/posts/' + this.post.id + '/comments');
this.comments = response.data;
}
}
}
WennekesComment.vue
export default {
name: "WennekesComment",
props: ['comment'],
data() {
return {
subComments: []
}
},
mounted() {
this.fetchSubcomments();
},
methods: {
fetchSubcomments() {
let response = this.$http.get('/api/comments/' + this.comment.id).then((result) => {
// console.log(result);
});
}
}
}
Template Logic
<wennekes-comment v-for="comment in comments" :key="comment.id" :comment="comment"></wennekes-comment>
<post v-if="!loading" :post="active" :key="active.id"/>
Thanks in advance, and my apologies if this question is somewhat unclear, I'm somewhat at a loss.
Regards,
Ryan
UPDATE
I think I got it to work. In Articles.vue, I have appended a key to the post component. I think this is Vue's way of knowing which specific instance of a component to update.
I think I got it to work. In Articles.vue, I have appended a key to the post component. I think this is Vue's way of knowing which specific instance of a component to update.

Why declared field in data with props initial value is undefined?

Since mutating a prop is an antipattern I do the following as one of the solutions to that, however when I console.log my new data field I get undefined. What's wrong?
export default {
name: "modal",
props: ["show"],
data() {
return {
sent: false,
mutableShow: this.show
};
},
methods: {
closeModal: function() {
this.mutableShow = false;
},
sendTeam: function() {
var self = this;
let clientId = JSON.parse(localStorage.getItem("projectClient")).id;
axios({
method: "get",
url: "/send-project-team/" + clientId,
data: data
})
.then(function(response) {
self.sent = true;
$("h3").text("Wooo");
$(".modal-body").text("Team was sent succesfully to client");
setTimeout(function() {
console.log(this.mutableShow);
self.closeModal();
}, 3000);
})
.catch(function(error) {
console.log(error);
});
}
}
};
Your timeout handler is establishing a new context. Instead of
setTimeout(function() {
console.log(this.mutableShow);
self.closeModal();
}, 3000);
you could use
setTimeout(() => {
console.log(this.mutableShow);
self.closeModal();
}, 3000);
And you'd need to make a similar change to
.then(function(response) {
to
.then(response => {
having said that, though, I'm not sure the code is going to behave as you might want it. Once the users closes the modal, it won't be possible to open it again since there is no way to make mutableShow equal to true.
Edited to add:
Since you're defining the self variable, you could also use that.
console.log(self.mutableShow);
Edited to add:
Without knowing specifically what behavior is intended, the best suggestion I can offer is to follow accepted Vue practices. Namely, after the AJAX request succeeds, emit a custom event. Have the parent component listen for that event and, when triggered, change the show prop.

Why is it considered poor practice to use Axios or HTTP calls in components?

In this article, it says:
While it’s generally poor practice, you can use Axios directly in your components to fetch data from a method, lifecycle hook, or whenever.
I am wondering why? I usually use lifecycle hooks a lot to fetch data (especially from created()). Where should we write the request calls?
Writing API methods directly in components increases code lines and make difficult to read.
As far as I believe the author is suggesting to separate API methods into a Service.
Let's take a case where you have to fetch top posts and operate on data. If you do that in component it is not re-usable, you have to duplicate it in other components where ever you want to use it.
export default {
data: () => ({
top: [],
errors: []
}),
// Fetches posts when the component is created.
created() {
axios.get(`http://jsonplaceholder.typicode.com/posts/top`)
.then(response => {
// flattening the response
this.top = response.data.map(item => {
title: item.title,
timestamp: item.timestamp,
author: item.author
})
})
.catch(e => {
this.errors.push(e)
})
}
}
So when you need to fetch top post in another component you have to duplicate the code.
Now let's put API methods in a Service.
api.js file
const fetchTopPosts = function() {
return axios.get(`http://jsonplaceholder.typicode.com/posts/top`)
.then(response => {
// flattening the response
this.top = response.data.map(item => {
title: item.title,
timestamp: item.timestamp,
author: item.author
})
}) // you can also make a chain.
}
export default {
fetchTopPosts: fetchTopPosts
}
So you use the above API methods in any components you wish.
After this:
import API from 'path_to_api.js_file'
export default {
data: () => ({
top: [],
errors: []
}),
// Fetches posts when the component is created.
created() {
API.fetchTopPosts().then(top => {
this.top = top
})
.catch(e => {
this.errors.push(e)
})
}
}
It's fine for small apps or widgets, but in a real SPA, it's better to abstract away your API into its own module, and if you use vuex, to use actions to call that api module.
Your component should not be concerned with how and from where its data is coming. The component is responsible for UI, not AJAX.
import api from './api.js'
created() {
api.getUsers().then( users => {
this.users = users
})
}
// vs.
created() {
axios.get('/users').then({ data }=> {
this.users = data
})
}
In the above example, your "axios-free" code is not really much shorter, but imagine what you could potentially keep out of the component:
handling HTTP errors, e.g. retrying
pre-formatting data from the server so it fits your component
header configuration (content-type, access token ...)
creating FormData for POSTing e.g. image files
the list can get long. all of that doesn't belong into the component because it has nothing to do with the view. The view only needs the resulting data or error message.
It also means that you can test your components and api independently.