VueJS,vue-form-generator Accessing $store from field callback - vue.js

I am using the vue-form-generator component. I am attempting to update a variable in the store with the response from a call back. What I believe the relevant code is (inside the schema:fields property):
{
type: 'submit',
buttonText: 'Save Updates',
styleClasses: ['custom-submit', 'custom-submit-primary', 'custom-submit-full'],
disabled () {
return this.errors.length > 0
},
validateBeforeSubmit: true,
onSubmit: function (model, schema) {
Vue.CustomSubmit('UserUpdate', model).then((response) => {
this.$store.user = response
})
}
}
From within the Vue.CustomSubmit().then((response)) => { ... }
I don't seem to be able to access the store.
It works slightly higher in scope, such as:
data () {
return {
avatar: this.$store.getters.user.avatar,
...
}

You cannot modify the $store directly. That's the most important part of the vuex concept. You need to use a mutation and commit it after you got the data.
Here it is described https://vuex.vuejs.org/en/mutations.html

Related

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

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

Vuex state changing with mutation - apollo graphql query

I have a form where the user edits the object - in my case, metadata about a story - after it is loaded from GraphQL. However, when I use my vue-router guard to check if the story has been changed, the story state is always the modified value.
Vuex story.js
...
getters: {
...formSubmit.getters,
getStory: (state) => {
return state.story},
getEditedStory: (state) => state.editedStory,
getStoryDescription: (state) => {
return state.story.description
}
},
mutations: {
...formSubmit.mutations,
setStory(state, payload) {
state.story = payload
},
setEditedStory(state, payload) {
state.editedStory = payload
}
},
...
Form component
export default {
...
apollo: {
story: {
query: gql`query GetStory($id: ID!) {
story(id: $id) {
name
summary
description
}
}`,
variables() {
return {
id: this.id
}
},
result({ data}) {
this.setText(data.story.description)
this.setStory(data.story)
this.setEditedStory(data.story)
},
}
},
...
In my form I have the values mapped with v-model:
<v-text-field
v-model="story.name"
class="mx-4"
label="Add your title..."
single-line
:counter="TITLE_TEXT_MAX_LENGTH"
outlined
solo
:disabled="updateOrLoadInProgress"
/>
However, for some reason whenever I call this.getStory its value is modified accordingly to the v-model. Why?
Although I still don't quite understand why, it seems like the changes to the apollo variable story affect the store.story values with using the mutations to set them.
I've modified the initial setters to be like this:
this.setText(data.story.description)
let loadedStory = {
name: data.story.name,
description: data.story.description,
summary: data.story.summary,
}
this.setStory(loadedStory)
this.setEditedStory(data.story)
},
which seems to prevent the state.story from following the changes to the apollo created story variable. This seems like unexpected behaviour, and I don't quite understand what part of javascript object assignment makes this work this way.

Vuex state change on object does not trigger rerender

I have a variable in the vuex store called permissions. And i want my component to trigger a rerender when the getPermissions changes. In the vue devtools i clearly see that the state has changed in the store, but the component stil get the old state from getPermissions. In order for me to see changes, I have to do a refresh. Has it something to do with the way i mutate it? or the fact that it is an object?
It looks like this when populated:
permissions: {
KS1KD933KD: true,
KD9L22F732: false
}
I use this method to do mutations on it and a getter to get it:
const getters = {
getPermissions: state => state.permissions
};
const mutations = {
set_recording_permissions(state, data) {
let newList = state.permissions;
newList[data.key] = data.bool;
Vue.set(state, 'permissions', newList);
}
};
And in the component i use mapGetters to get access to it
computed: {
...mapGetters('agentInfo',['getPermissions'])
}
In order to update the permissions value i use this action (it does require a succesfull api request before updating the value) :
const actions = {
async setRecordingPermissions({ commit }, data) {
let body = {
agentId: data.userName,
callId: data.callId,
allowUseOfRecording: data.allowUseOfRecording
};
try {
await AgentInfoAPI.editRecordingPermissions(body).then(() => {
commit('set_recording_permissions', { key: data.callId, bool: data.allowUseOfRecording });
commit('set_agent_info_message', {
type: 'success',
text: `Endret opptaksrettigheter`
});
});
} catch (error) {
console.log(error);
commit('set_agent_info_message', {
type: 'error',
text: `Request to ${error.response.data.path} failed with ${error.response.status} ${error.response.data.message}`
});
}
}
}
Since the getter only returns state variable you should use mapState, if you want to access it directly.
computed: mapState(['permissions'])
However, you can also use mapGetters, but then in your template, have to use getPermissions and not permissions.
Example template:
<ul id="permissions">
<li v-for="permission in getPermissions">
{{ permission }}
</li>
</ul>
If you have done this it is probably an issue with the object reference. You use Vue.set, but you set the same object reference. You have to create a new object or set the key you want to update directly.
new object
let newList = { ...state.permissions };
Vue.set
Vue.set(state.permission, data.key, data.value);
I don't know what the rest of you code looks like, but you will need to use actions to correctly mutate you store.
For example:
const actions = {
setName({ commit }, name) {
commit('setName', name);
},
}

How to react once to a change in two or more watched properties in vuejs

There are many use cases for this but the one I'm dealing with is as follows. I have a page with a table, and two data properties, "page" and "filters". When either of these two variables are updated I need to fetch results from the server again.
However, there is no way as far as I can see to watch two variables and react only once, especially in the complicated instance updating filters should reset page to zero.
javascript
data: {
return {
page: 0,
filters: {
searchText: '',
date: ''
}
}
},
watch: {
page (nv) {
this.fetchAPI()
},
filters: {
deep: true,
handler (nv) {
this.page = 0
this.fetchAPI()
}
}
},
methods: {
fetchAPI () {
// fetch data via axios here
}
}
If i update filters, its going to reset page to 0 and call fetchAPI() twice. However this seems like the most intuitive way to have a page with a table in it? filters should reset page to zero as you may be on page 500 and then your filters cause there to only be 1 page worth of results, and a change to either page or filters must call the api again.
Interested to see how others must be tackling this exact same problem reactively?
Take into the rule - watchers are the "last hope". You must not use them until you have other ways.
In your case, you could use events. This way the problem will go by itself:
Add #click="onPageChange" event to the page button (or whatever do you use).
Add #change="onFilterChange" event to the filter component (or whatever do you use). You can also use #click="onFilterChange" with some additional code to detect changes. Still, I am pretty sure you must have something like #change on the filter component.
Then your code will look like:
data: {
return {
page: 0,
filters: {
searchText: '',
date: ''
}
}
},
methods: {
onPageChange () {
this.fetchAPI()
},
onFilterChange () {
this.page = 0
this.fetchAPI()
},
fetchAPI () {
// fetch data via axios here
}
}
In this case, the onFilterChange will change the page data but will not trigger the onPageChange method. So your problem will not exist.
Since the filters object is really complicated and has many options i have decided to keep it on a watcher that triggers setting page to zero and reloading the api. I now solve the problem as stated below. The 'pauseWatcher' data variable is a bit messy and needing to disable it in nextTick seems unideal but its a small price to pay for not having to manually hook up each and every filters input (some of them are far more complex than one input, like a date filter between two dates) and have them each emit onChange events that trigger reloading the api. It seems sad Vuejs doesnt have a lifecycle hook where you can access data properties and perhaps route paramters and make final changes to them before the watchers and computed properties turn on.
data: {
return {
pauseWatcher: true,
page: 0,
filters: {
searchText: '',
date: ''
}
}
},
watch: {
filters: {
deep: true,
handler (nv) {
if (this.pauseWatcher) {
return
}
this.page = 0
this.fetchAPI()
}
}
},
methods: {
fetchAPI () {
// fetch data via axios here
},
goToPage(page) {
this.page = page
this.fetchAPI()
},
decodeFilters () {
// decode filters from base64 URL string
}
},
created () {
this.pauseWatcher = true
this.page = Number(this.$route.query.page) || 0
this.filters = this.decodeFilters(this.$route.query.filters)
this.$nextTick(() => {
this.pauseWatcher = false
})
}

Using Vue-Resource in Vuex store; getting maximum call stack size error

I'm trying to pull an array from my api to a component using vuex but I'm at a loss and probably in over my head in attempting this. With the api being accessed directly from a component I had it set up like this and it was fine:
data () {
return {
catalog:[],
}
},
created() {
this.$http.get('https://example.net/api.json').then(data => {
this.catalog = data.body[0].threads;
})
}
For reference the json looks similar to this:
[{
"threads": [{
"no: 12345,
"comment": "comment here",
"name": "user"
}, {
"no: 67890,
"comment": "another comment here",
"name": "user2"
}],
//this goes on for about 15 objects more in the array
"page": 0
}]
When I move this all to store I'm losing grasp on how to actually make this work. I've used vuex before just never with vue-resource.
//store.js
state: {
catalog: []
},
actions: {
getCatalog({commit}){
Vue.http.get('https://example.net/api.json').then(response => {
commit('LOAD_CATALOG', response.data.data)
});
}
},
mutations: {
LOAD_CATALOG (state) {
state.catalog.push(state.catalog)
}
},
getters: {
catalog: state => state.catalog,
}
//component.vue
created () {
this.$store.dispatch('getCatalog')
},
computed: {
catalog () {
return this.$store.getters.catalog
}
}
I'm aware this is wrong and I'm getting max call stack size errors. How can I get the same results as posted in the example above (this.catalog = data.body[0].threads;) when I put everything in store?
Let me know if anything needs clarification! I'm still pretty new at using Vue 2.0.
Your main issue is with your mutation.
Mutations are synchronous updates to the state, therefore you are correctly calling it from the action (where you process your async request) but you aren't passing the mutation anything to place in the state. Mutations accept arguments, so your LOAD_CATALOG mutation would accept the catalogData, i.e.
mutations: {
LOAD_CATALOG (state, catalogData) {
state.catalog = catalogData
}
},
Also if you are using vue resource for Vue 2 then you should be passing the body of the response to the mutation, i.e.
getCatalog({commit}){
Vue.http.get('https://example.net/api.json').then(response => {
commit('LOAD_CATALOG', response.body[0].threads)
});
}
The next issue is you don't need the getter, getters allow us to compute a derived state, you don't need them just to return an existing state (in your case catalog). A basic example of where you may use a getter would be to add 1 to a counter stored in state, i.e.
getters: {
counterAdded: state => state.counter + 1,
}
Once you've made these changes things will look a bit more like below:
//store.js
state: {
catalog: []
},
actions: {
getCatalog({commit}){
Vue.http.get('https://example.net/api.json').then(response => {
commit('LOAD_CATALOG', response.body[0].threads)
});
}
},
mutations: {
LOAD_CATALOG (state, catalogData) {
state.catalog = catalogData
}
},
//component.vue
created () {
this.$store.dispatch('getCatalog')
},
computed: {
catalog () {
return this.$store.state.catalog
}
}